Opening a Position
- Rust
- TypeScript Kit
- TypeScript Legacy
Opening a position in liquidity pools on Orca is a fundamental step for providing liquidity and earning fees. In this guide, we'll explore how to open a position in both Splash Pools and Concentrated Liquidity Pools, their differences, and which approach is suitable for different use cases.
1. Introduction to Positions in Pools
A position in a liquidity pool represents your contribution of liquidity, which allows traders to swap between tokens while you earn a share of the trading fees. When you open a position, you decide how much liquidity to add, and this liquidity can later be adjusted or removed.
-
Splash Pools: Provide liquidity without specifying a price range. Ideal for those seeking a simple way to start providing liquidity.
-
Concentrated Liquidity Pools: Allow you to provide liquidity within a specified price range, enabling higher capital efficiency but requiring more advanced management.
Upon creation of the position, an NFT will be minted to represent ownership of the position. This NFT is used by the program to verify your ownership when adjusting liquidity, harvesting rewards, or closing the position. For more information, refer to Tokenized Positions.
⚠️ Risk of Divergence loss: The ratio of Token A to Token B that you deposit as liquidity is determined by several factors, including the current price. As trades occur against the pool, the amounts of Token A and Token B in the pool — and in your position — will change, which affects the price of the tokens relative to each other. This can work to your advantage, but it may also result in the combined value of your tokens (including any earned fees and rewards) being lower than when you initially provided liquidity.
2. Getting Started Guide
Before opening a position, ensure you have completed the environment setup:
- RPC Setup: Use a Solana RPC client to communicate with the blockchain.
- Wallet Creation: Create a wallet to interact with the Solana network.
- Devnet Airdrop: Fund your wallet with a Solana Devnet airdrop to cover transaction fees.
For more details, refer to our Environment Setup Guide
Opening a Position in Splash Pools
- Pool Address: Provide the address of the Splash Pool where you want to open a position.
- Liquidity Parameters: Choose how you want to provide liquidity. You only need to provide one of these parameters, and the function will compute the others in the returned quote based on the current price of the pool:
liquidity
: Specify the liquidity value to provide.tokenA
: Specify the amount of token A (first token in the pool).tokenB
: Specify the amount of token B (second token in the pool).
- Slippage Tolerance: Set the maximum slippage tolerance (optional, defaults to 1%). Slippage refers to the difference between the expected price and the actual price at which the transaction is executed. A lower slippage tolerance reduces the risk of price changes during the transaction but may lead to failed transactions if the market moves too quickly.
- Funder: This will be your wallet, which will fund the transaction.
- Create Instructions: Use the appropriate function to generate the necessary instructions.
use orca_whirlpools::{
open_full_range_position_instructions, set_whirlpools_config_address, IncreaseLiquidityParam, WhirlpoolsConfigInput
};
use solana_client::nonblocking::rpc_client::RpcClient;
use solana_sdk::pubkey::Pubkey;
use std::str::FromStr;
use solana_sdk::signature::Signer;
use orca_tx_sender::{
build_and_send_transaction,
set_rpc, get_rpc_client
};
use solana_sdk::commitment_config::CommitmentLevel;
use crate::utils::load_wallet;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
set_rpc("https://api.devnet.solana.com").await?;
set_whirlpools_config_address(WhirlpoolsConfigInput::SolanaDevnet).unwrap();
let wallet = load_wallet();
let whirlpool_address = Pubkey::from_str("3KBZiL2g8C7tiJ32hTv5v3KM7aK9htpqTw4cTXz1HvPt").unwrap();
let param = IncreaseLiquidityParam::TokenA(10);
let rpc = get_rpc_client()?;
let result = open_full_range_position_instructions(
&rpc,
whirlpool_address,
param,
Some(100),
Some(wallet.pubkey())
).await?;
println!("Quote token mac B: {:?}", result.quote.token_max_b);
println!("Initialization cost: {:?}", result.initialization_cost);
println!("Position mint: {:?}", result.position_mint);
let signature = build_and_send_transaction(
result.instructions,
&[&wallet],
Some(CommitmentLevel::Confirmed),
None, // No address lookup tables
).await?;
println!("Transaction sent: {}", signature);
Ok(())
}
Opening a Position in Concentrated Liquidity Pools
- Pool Address: Provide the address of the Concentrated Liquidity Pool where you want to open a position.
- Liquidity Parameters: Choose how you want to provide liquidity. You only need to provide one of these parameters, and the function will compute the others in the returned quote based on the price range and the current price of the pool:
liquidity
: Specify the liquidity value to provide.tokenA
: Specify the amount of token A (first token in the pool).tokenB
: Specify the amount of token B (second token in the pool).
- Price Range: Set the lower and upper bounds of the price range within which your liquidity will be active. The current price and the specified price range will influence the quote amounts. If the current price is in the middle of your price range, the ratio of token A to token B will reflect that price. However, if the current price is outside your range, you will only deposit one token, resulting in one-sided liquidity. Note that your position will only earn fees when the price falls within your selected price range, so it's important to choose a range where you expect the price to remain active.
- Slippage Tolerance: Set the maximum slippage tolerance (optional, defaults to 1%). Slippage refers to the difference between the expected token amounts and the actual amounts deposited into the liquidity pool. A lower slippage tolerance reduces the risk of depositing more tokens than expected but may lead to failed transactions if the market moves too quickly. For example, if you expect to deposit 100 units of Token A and 1,000 units of Token B, with a 1% slippage tolerance, the maximum amounts would be 101 Token A and 1,010 Token B.
- Funder: This can be your wallet, which will fund the pool initialization. If the funder is not specified, the default wallet will be used. You can configure the default wallet through the SDK.
- Create Instructions: Use the appropriate function to generate the necessary instructions.
use orca_whirlpools::{
open_position_instructions, set_whirlpools_config_address, IncreaseLiquidityParam, WhirlpoolsConfigInput
};
use solana_client::nonblocking::rpc_client::RpcClient;
use solana_sdk::pubkey::Pubkey;
use std::str::FromStr;
use solana_sdk::signature::Signer;
use orca_tx_sender::{
build_and_send_transaction,
set_rpc, get_rpc_client
};
use solana_sdk::commitment_config::CommitmentLevel;
use crate::utils::load_wallet;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
set_rpc("https://api.devnet.solana.com").await?;
set_whirlpools_config_address(WhirlpoolsConfigInput::SolanaDevnet).unwrap();
let wallet = load_wallet();
let whirlpool_address = Pubkey::from_str("3KBZiL2g8C7tiJ32hTv5v3KM7aK9htpqTw4cTXz1HvPt").unwrap();
let param = IncreaseLiquidityParam::TokenA(10);
let rpc = get_rpc_client()?;
let result = open_position_instructions(
&rpc,
whirlpool_address,
0.001,
100.0,
param,
Some(100),
Some(wallet.pubkey())
).await?;
println!("Quote token max B: {:?}", result.quote.token_est_b);
println!("Initialization cost: {:?}", result.initialization_cost);
println!("Position mint: {:?}", result.position_mint);
let signature = build_and_send_transaction(
result.instructions,
&[&wallet],
Some(CommitmentLevel::Confirmed),
None, // No address lookup tables
).await?;
println!("Transaction sent: {}", signature);
Ok(())
}
⚠️ You cannot use this function on Splash Pools, as this function is specifically for Concentrated Liquidity Pools.
3. Usage examples
Opening a Position in a Splash Pool
Suppose you want to provide 1,000,000 tokens of Token A at a price of 0.0001 SOL. You will also need to provide 100 SOL as Token B to match the price. By using the SDK to open full range positions, you ensure that your liquidity is spread evenly across all price levels. This approach is ideal if you are launching a new token and want to facilitate easy swaps for traders.
Opening a Position in a Concentrated Liquidity Pool
If you want to maximize capital efficiency, you can open a position in a Concentrated Liquidity Pool. For example, if the current price is at 0.01 and you want to maximize profitability, you could use the SDK to deposit liquidity between the price range of 0.009 and 0.011. This approach allows you to focus your liquidity in a narrow range, making it more effective and potentially more profitable.
Next Steps
After opening a position, you can:
- Add or Remove Liquidity: Adjust the amount of liquidity in your position based on market conditions.
- Harvest Rewards: Collect rewards and fees without closing the position.
- Monitor Performance: Track your position's performance and earned fees.
- Close Position: When you decide to exit, close the position and withdraw the provided tokens along with any earned fees.
Opening a position in liquidity pools on Orca is a fundamental step for providing liquidity and earning fees. In this guide, we'll explore how to open a position in both Splash Pools and Concentrated Liquidity Pools, their differences, and which approach is suitable for different use cases.
1. Introduction to Positions in Pools
A position in a liquidity pool represents your contribution of liquidity, which allows traders to swap between tokens while you earn a share of the trading fees. When you open a position, you decide how much liquidity to add, and this liquidity can later be adjusted or removed.
-
Splash Pools: Provide liquidity without specifying a price range. Ideal for those seeking a simple way to start providing liquidity.
-
Concentrated Liquidity Pools: Allow you to provide liquidity within a specified price range, enabling higher capital efficiency but requiring more advanced management.
Upon creation of the position, an NFT will be minted to represent ownership of the position. This NFT is used by the program to verify your ownership when adjusting liquidity, harvesting rewards, or closing the position. For more information, refer to Tokenized Positions.
⚠️ Risk of Divergence loss: The ratio of Token A to Token B that you deposit as liquidity is determined by several factors, including the current price. As trades occur against the pool, the amounts of Token A and Token B in the pool — and in your position — will change, which affects the price of the tokens relative to each other. This can work to your advantage, but it may also result in the combined value of your tokens (including any earned fees and rewards) being lower than when you initially provided liquidity.
2. Getting Started Guide
Before opening a position, ensure you have completed the environment setup:
- RPC Setup: Use a Solana RPC client to communicate with the blockchain.
- Wallet Creation: Create a wallet to interact with the Solana network.
- Devnet Airdrop: Fund your wallet with a Solana Devnet airdrop to cover transaction fees.
For more details, refer to our Environment Setup Guide
Opening a Position in Splash Pools
- Pool Address: Provide the address of the Splash Pool where you want to open a position.
- Liquidity Parameters: Choose how you want to provide liquidity. You only need to provide one of these parameters, and the function will compute the others in the returned quote based on the current price of the pool:
liquidity
: Specify the liquidity value to provide.tokenA
: Specify the amount of token A (first token in the pool).tokenB
: Specify the amount of token B (second token in the pool).
- Slippage Tolerance: Set the maximum slippage tolerance (optional, defaults to 1%). Slippage refers to the difference between the expected price and the actual price at which the transaction is executed. A lower slippage tolerance reduces the risk of price changes during the transaction but may lead to failed transactions if the market moves too quickly.
- Funder: This will be your wallet, which will fund the transaction.
- Create Instructions: Use the appropriate function to generate the necessary instructions.
import { openFullRangePositionInstructions, setWhirlpoolsConfig } from '@orca-so/whirlpools';
import { createSolanaRpc, devnet, address } from '@solana/kit';
import { loadWallet } from './utils';
await setWhirlpoolsConfig('solanaDevnet');
const devnetRpc = createSolanaRpc(devnet('https://api.devnet.solana.com'));
const wallet = await loadWallet(); // load your wallet
const whirlpoolAddress = address("3KBZiL2g8C7tiJ32hTv5v3KM7aK9htpqTw4cTXz1HvPt"); // SOL/devUSDC
const param = { tokenA: 10n };
const { quote, instructions, initializationCost, positionMint, callback: sendTx } = await openFullRangePosition(
devnetRpc,
whirlpoolAddress,
param,
100,
wallet
);
// Use the callback to submit the transaction
const txId = await sendTx();
console.log(`Quote token max B: ${quote.tokenMaxB}`);
console.log(`Initialization cost: ${initializationCost}`);
console.log(`Position mint: ${positionMint}`);
console.log(`Transaction ID: ${txId}`);
Opening a Position in Concentrated Liquidity Pools
- Pool Address: Provide the address of the Concentrated Liquidity Pool where you want to open a position.
- Liquidity Parameters: Choose how you want to provide liquidity. You only need to provide one of these parameters, and the function will compute the others in the returned quote based on the price range and the current price of the pool:
liquidity
: Specify the liquidity value to provide.tokenA
: Specify the amount of token A (first token in the pool).tokenB
: Specify the amount of token B (second token in the pool).
- Price Range: Set the lower and upper bounds of the price range within which your liquidity will be active. The current price and the specified price range will influence the quote amounts. If the current price is in the middle of your price range, the ratio of token A to token B will reflect that price. However, if the current price is outside your range, you will only deposit one token, resulting in one-sided liquidity. Note that your position will only earn fees when the price falls within your selected price range, so it's important to choose a range where you expect the price to remain active.
- Slippage Tolerance: Set the maximum slippage tolerance (optional, defaults to 1%). Slippage refers to the difference between the expected token amounts and the actual amounts deposited into the liquidity pool. A lower slippage tolerance reduces the risk of depositing more tokens than expected but may lead to failed transactions if the market moves too quickly. For example, if you expect to deposit 100 units of Token A and 1,000 units of Token B, with a 1% slippage tolerance, the maximum amounts would be 101 Token A and 1,010 Token B.
- Funder: This can be your wallet, which will fund the pool initialization. If the funder is not specified, the default wallet will be used. You can configure the default wallet through the SDK.
- Create Instructions: Use the appropriate function to generate the necessary instructions.
import { openPositionInstructions, setWhirlpoolsConfig } from '@orca-so/whirlpools';
import { createSolanaRpc, devnet, address } from '@solana/kit';
import { loadWallet } from './utils';
await setWhirlpoolsConfig('solanaDevnet');
const devnetRpc = createSolanaRpc(devnet('https://api.devnet.solana.com'));
const wallet = await loadWallet(); // load your wallet
const whirlpoolAddress = address("3KBZiL2g8C7tiJ32hTv5v3KM7aK9htpqTw4cTXz1HvPt"); // SOL/devUSDC
const param = { tokenA: 10n };
const { quote, instructions, initializationCost, positionMint, callback: sendTx } = await openPosition(
devnetRpc,
whirlpoolAddress,
param,
0.001,
100.0,
100,
wallet
);
// Use the callback to submit the transaction
const txId = await sendTx();
console.log(`Quote token max B: ${quote.tokenEstB}`);
console.log(`Initialization cost: ${initializationCost}`);
console.log(`Position mint: ${positionMint}`);
console.log(`Transaction ID: ${txId}`);
⚠️ You cannot use this function on Splash Pools, as this function is specifically for Concentrated Liquidity Pools.
3. Usage examples
Opening a Position in a Splash Pool
Suppose you want to provide 1,000,000 tokens of Token A at a price of 0.0001 SOL. You will also need to provide 100 SOL as Token B to match the price. By using the SDK to open full range positions, you ensure that your liquidity is spread evenly across all price levels. This approach is ideal if you are launching a new token and want to facilitate easy swaps for traders.
Opening a Position in a Concentrated Liquidity Pool
If you want to maximize capital efficiency, you can open a position in a Concentrated Liquidity Pool. For example, if the current price is at 0.01 and you want to maximize profitability, you could use the SDK to deposit liquidity between the price range of 0.009 and 0.011. This approach allows you to focus your liquidity in a narrow range, making it more effective and potentially more profitable.
Next Steps
After opening a position, you can:
- Add or Remove Liquidity: Adjust the amount of liquidity in your position based on market conditions.
- Harvest Rewards: Collect rewards and fees without closing the position.
- Monitor Performance: Track your position's performance and earned fees.
- Close Position: When you decide to exit, close the position and withdraw the provided tokens along with any earned fees.
Positions in Whirlpools are tracked with a minted NFT in the user's wallet.
The usual action of opening a position consists of two instruction calls
initializeTickArray
to initialize the tick arrays that would host your desired ticks for your position if they do not exist yet.Whirlpool.openPosition
orWhirlpool.openPositionWithMetadata
to mint the position and define the tick rangeincreaseLiquidity
to transfer tokens from your wallet into a position.
The Whirlpool.openPosition
function now supports both traditional and Token2022-based position NFTs. To utilize Token2022, provide the Token2022 ProgramId as the tokenProgramId
parameter when calling openPosition
. This will mint the NFT using Token2022, which leverages the MetadataPointer and TokenMetadata extensions, eliminating the need for Metaplex metadata accounts.
Opening Position with Metadata
By using Whirlpool.openPositionWithMetadata
, users have the option of appending Metaplex metadata onto the Token Program position NFT. Doing so will allow the token to be identifiable in tracking websites or wallets as a Whirlpool NFT. The drawback is it will require more compute-budget and will incurr Metaplex fees of 0.01 SOL.
Initialize Tick Array accounts if needed
For liquidity to exist in the Whirlpool, the tick-array that contains that particular tick must be initialized. Calculate the start_index of the required tick array and use the initialize_tick_array
instruction to initialize it.
More often than not, tick-arrays are already created. But if you want your code to be defensive, you should do a check prior to invoking open_position
. To understand more on how Tick-Arrays work in Whirlpools, read here.
const tickArrayPda = PDAUtil.getTickArray(
this.ctx.program.programId,
this.address,
startTick
);
// Check if tick array exists
const fetcher = new AccountFetcher(...);
const ta = await fetcher.getTickArray(tickArrayPda.publicKey, true);
// Exit if it exists
if (!!ta) {
return;
}
// Construct Init Tick Array Ix
const tx = toTx(ctx, WhirlpoolIx.initTickArrayIx(this.ctx.program, {
startTick,
tickArrayPda,
whirlpool: this.address,
funder: !!funder ? AddressUtil.toPubKey(funder) : this.ctx.wallet.publicKey,
}));
await tx.buildAndExecute();
Open Position with WhirlpoolClient
WhirlpoolClient's openPosition
method bundles the open and increase liquidity instructions into a single transaction for you. Below is a code sample to create a position for the SOL/USDC pool at the price between 150, with the intention to deposit 50 SOL into the position.
// Derive the Whirlpool address
const poolAddress = PDAUtil.getWhirlpool(
WHIRLPOOL_PROGRAM_ID,
ORCA_WHIRLPOOLS_CONFIG,
SOL_MINT,
USDC_MINT,
64
);
// Load everything that you need
const client = buildWhirlpoolClient(context, fetcher);
const pool = await client.getPool(poolAddress.publicKey);
const poolData = pool.getData();
const poolTokenAInfo = pool.getTokenAInfo();
const poolTokenBInfo = pool.getTokenBInfo();
// Derive the tick-indices based on a human-readable price
const tokenADecimal = poolTokenAInfo.decimals;
const tokenBDecimal = poolTokenBInfo.decimals;
const tickLower = TickUtil.getInitializableTickIndex(
PriceMath.priceToTickIndex(new Decimal(98), tokenADecimal, tokenBDecimal),
poolData.tickSpacing
);
const tickUpper = TickUtil.getInitializableTickIndex(
PriceMath.priceToTickIndex(new Decimal(150), tokenADecimal, tokenBDecimal),
poolData.tickSpacing
);
// Get a quote on the estimated liquidity and tokenIn (50 tokenA)
const quote = increaseLiquidityQuoteByInputToken(
poolTokenAInfo.mint,
new Decimal(50),
tickLower,
tickUpper,
Percentage.fromFraction(1, 100),
pool
);
// Evaluate the quote if you need
const {tokenMaxA, tokenMaxB} = quote
// Construct the open position & increase_liquidity ix and execute the transaction.
const { positionMint, tx } = await pool.openPosition(
lowerTick,
upperTick,
quote
);
const txId = await tx.buildAndExecute();
// Fetch the newly created position with liquidity
const position = await client.getPosition(
PDAUtil.getPosition(WHIRLPOOL_PROGRAM_ID, positionMint).publicKey
)
The Manual way
Follow the instructions below if you would like to have more control over your instruction building process. Note that open_position
does not add liquidity to a position. Follow the next article "Modify Liquidity" to add liquidity.
Determine position parameters
To open a position against a Whirlpool, you must first define certain parameters of your position to invoke the open_position
instruction.
WhirlpoolKey
- The public key for the Whirlpool that the position will host liquidity in.tickLowerIndex
,tickUpperIndex
- The tick index bounds for the position. Must be an initializable index.positionMintAddress
- A generated empty Keypair that will be initialized to a token mint.positionPda
- Derived address of the position account viagetPositionPda
positionTokenAccountAddress
- This is the account that will hold the minted position token. It is the associated token address of the position-mint.
const positionMintKeypair = Keypair.generate();
const positionPda = getPositionPda(programId, positionMintKeypair.publicKey);
const metadataPda = getPositionMetadataPda(positionMintKeypair.publicKey);
const positionTokenAccountAddress = await deriveATA(
provider.wallet.publicKey,
positionMintKeypair.publicKey
);
const positionIx = toTx(ctx, WhirlpoolIx.openPositionWithMetadataIx(ctx.program, {
funder: provider.wallet.publicKey,
ownerKey: provider.wallet.publicKey,
positionPda,
metadataPda,
positionMintAddress: positionMintKeypair.publicKey,
positionTokenAccountAddress,
whirlpoolKey: toPubKey(poolAddress),
tickLowerIndex,
tickUpperIndex,
})).addSigner(positionMintKeypair).buildAndExecute();
Once your position is open, proceed to the next section to add liquidity.
Common Errors
InvalidTickIndex
(0x177a)- tickLowerIndex is higher than upper tickUpperIndex
- Some tick indices is not an initializable index (not a multiple of tickSpacing). Use
TickUtil.getInitializableTickIndex
to get the closest initializable tick to your index. - Some tick indices is out of bounds
NotRentExempt
(0x0)- Usually, the TickArray that houses your tickLowerIndex or tickUpperIndex has not been initialized. Use the
WhirlpoolClient.initTickArrayForTicks
orWhirlpoolIx.initTickArrayIx
to initialize the array at the derived startTickIndex. - Alternatively, if this failure is from
init_tick_array
, the tick array has already been initialized.
- Usually, the TickArray that houses your tickLowerIndex or tickUpperIndex has not been initialized. Use the